/**
 * Copyright 2013 Alexey Ragozin
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gridkit.jvmtool.cmd;

import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Iterator;
import java.util.Set;

import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.RuntimeMBeanException;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.CompositeType;
import javax.management.openmbean.TabularData;

import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonGenerator;
import org.gridkit.jvmtool.JmxConnectionInfo;
import org.gridkit.jvmtool.cli.CommandLauncher;
import org.gridkit.jvmtool.cli.CommandLauncher.CmdRef;

import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParametersDelegate;

/**
 * MBean to JSON dump.
 */
public class MxDumpCmd implements CmdRef {

	@Override
	public String getCommandName() {
		return "mxdump";
	}

	@Override
	public Runnable newCommand(CommandLauncher host) {
		return new MxDump(host);
	}
	
	public static class MxDump implements Runnable {
		
		@ParametersDelegate
		private CommandLauncher host;
		
		@Parameter(names = {"-q", "--query"}, description = "Query to filter MBeans")
		private String query;
		
		@ParametersDelegate
		private JmxConnectionInfo conn;

		public MxDump(CommandLauncher host) {
			this.host = host;
			this.conn = new JmxConnectionInfo(host);
		}

		@Override
		public void run() {
			
	        try {
	        	ObjectName q = null;
	        	if (query != null) {
	        		q = new ObjectName(query);
	        	}
				MBeanServerConnection jmx = conn.getMServer();
				JsonFactory jsonFactory = new JsonFactory();
				JsonGenerator jg = jsonFactory.createJsonGenerator(System.out);
				jg.useDefaultPrettyPrinter();
				jg.writeStartObject();
				listBeans(q, jg, jmx);
				jg.writeEndObject();
				jg.close();
				System.out.println();
				System.out.flush();
			} catch (Exception e) {
				host.fail(e.toString());
			}			
		}
		
	    private static void listBeans(ObjectName query, JsonGenerator jg, MBeanServerConnection mBeanServer) throws Exception {
	        Set<ObjectName> names = null;
	        names = mBeanServer.queryNames(query, null);

	        jg.writeArrayFieldStart("beans");
	        Iterator<ObjectName> it = names.iterator();
	        while (it.hasNext()) {
	            ObjectName oname = it.next();
	            MBeanInfo minfo;
	            String code = "";
	            minfo = mBeanServer.getMBeanInfo(oname);
	            code = minfo.getClassName();
	            String prs = "";
	            if ("org.apache.commons.modeler.BaseModelMBean".equals(code)) {
	                prs = "modelerType";
	                code = (String) mBeanServer.getAttribute(oname, prs);
	            }

	            jg.writeStartObject();
	            jg.writeStringField("name", oname.toString());

	            jg.writeStringField("modelerType", code);

	            MBeanAttributeInfo attrs[] = minfo.getAttributes();
	            for (int i = 0; i < attrs.length; i++) {
	                writeAttribute(mBeanServer, jg, oname, attrs[i]);
	            }
	            jg.writeEndObject();
	        }
	        jg.writeEndArray();
	    }

	    private static void writeAttribute(MBeanServerConnection mBeanServer, JsonGenerator jg, ObjectName oname, MBeanAttributeInfo attr) throws IOException {
	        if (!attr.isReadable()) {
	            return;
	        }
	        String attName = attr.getName();
	        if ("modelerType".equals(attName)) {
	            return;
	        }
	        if (attName.indexOf("=") >= 0 || attName.indexOf(":") >= 0
	                || attName.indexOf(" ") >= 0) {
	            return;
	        }
	        Object value = null;
	        try {
	            value = mBeanServer.getAttribute(oname, attName);
	        } catch (AttributeNotFoundException e) {
	            //Ignored the attribute was not found, which should never happen because the bean
	            //just told us that it has this attribute, but if this happens just don't output
	            //the attribute.
	            return;
	        } catch (MBeanException e) {
	            //The code inside the attribute getter threw an exception so log it, and
	            // skip outputting the attribute
	            error("getting attribute "+attName+" of "+oname+" threw an exception", e);
	            return;
	        } catch (RuntimeMBeanException e) {
	            // The code inside the attribute getter threw an exception, so we skip
	            // outputting the attribute. We will log the exception in certain cases,
	            // but suppress the log message in others. See OPSAPS-5449 for more info.
	            if (!(e.getCause() instanceof UnsupportedOperationException)) {
	                error("getting attribute " + attName + " of " + oname + " threw " +
	                        "an exception", e);
	            }
	            return;
	        } catch (RuntimeException e) {
	            //For some reason even with an MBeanException available to them Runtime exceptions
	            //can still find their way through, so treat them the same as MBeanException
	            error("getting attribute "+attName+" of "+oname+" threw an exception", e);
	            return;
	        } catch (ReflectionException e) {
	            //This happens when the code inside the JMX bean (setter?? from the java docs)
	            //threw an exception, so log it and skip outputting the attribute
	            error("getting attribute "+attName+" of "+oname+" threw an exception", e);
	            return;
	        } catch (InstanceNotFoundException e) {
	            //Ignored the mbean itself was not found, which should never happen because we
	            //just accessed it (perhaps something unregistered in-between) but if this
	            //happens just don't output the attribute.
	            return;
	        }

	        writeAttribute(jg, attName, value);
	    }

	    private static void error(String string, Throwable e) {
	        System.err.print(string);
	        e.printStackTrace(System.err); 
	    }



	    private static void writeAttribute(JsonGenerator jg, String attName, Object value) throws IOException {
	        jg.writeFieldName(attName);
	        writeObject(jg, value);
	    }

	    private static void writeObject(JsonGenerator jg, Object value) throws IOException {
	        if(value == null) {
	            jg.writeNull();
	        } else {
	            Class<?> c = value.getClass();
	            if (c.isArray()) {
	                jg.writeStartArray();
	                int len = Array.getLength(value);
	                for (int j = 0; j < len; j++) {
	                    Object item = Array.get(value, j);
	                    writeObject(jg, item);
	                }
	                jg.writeEndArray();
	            } else if(value instanceof Number) {
	                Number n = (Number)value;
	                jg.writeNumber(n.toString());
	            } else if(value instanceof Boolean) {
	                Boolean b = (Boolean)value;
	                jg.writeBoolean(b);
	            } else if(value instanceof CompositeData) {
	                CompositeData cds = (CompositeData)value;
	                CompositeType comp = cds.getCompositeType();
	                Set<String> keys = comp.keySet();
	                jg.writeStartObject();
	                for(String key: keys) {
	                    writeAttribute(jg, key, cds.get(key));
	                }
	                jg.writeEndObject();
	            } else if(value instanceof TabularData) {
	                TabularData tds = (TabularData)value;
	                jg.writeStartArray();
	                for(Object entry : tds.values()) {
	                    writeObject(jg, entry);
	                }
	                jg.writeEndArray();
	            } else {
	                jg.writeString(value.toString());
	            }
	        }
	    }
	}
}
